#!/usr/bin/env node

/**
 * TinyPNG 官方 API 图片批量压缩工具
 * 
 * 功能特性：
 * - 使用 TinyPNG 官方 API 进行压缩
 * - 递归扫描目录下的所有图片文件
 * - 支持 jpg、jpeg、png、webp 格式
 * - 并发处理提高效率
 * - 失败文件自动重试机制
 * - 保持原有目录结构
 * - 支持复制非压缩格式文件
 * - 显示详细的压缩统计信息
 * 
 * @version 2.0.0
 */

const fs = require('fs');
const path = require('path');
const tinify = require('tinify');
const chalk = require('chalk');
const ora = require('ora');

// 加载环境变量
require('dotenv').config();

// 配置
const config = {
  // 支持多个 API key，用逗号分隔
  API_KEYS: process.env.TINYPNG_API_KEY ? process.env.TINYPNG_API_KEY.split(',').map(k => k.trim()).filter(k => k) : [],
  SOURCE_ROOT: process.env.SOURCE_ROOT || './static',
  OUTPUT_ROOT: process.env.OUTPUT_ROOT || './compressed',
  REPLACE_ORIGINAL: process.env.REPLACE_ORIGINAL === 'true',
  FAILED_DIR: process.env.FAILED_DIR || './failed_images',
  SUPPORTED_FORMATS: ['.jpg', '.jpeg', '.png', '.webp'],
  FILE_DELAY: parseInt(process.env.FILE_DELAY) || 200,
  PRESERVE_DIR_STRUCTURE: process.env.PRESERVE_DIR_STRUCTURE !== 'false',
  SKIP_HIDDEN_DIRS: process.env.SKIP_HIDDEN_DIRS !== 'false',
  COPY_UNSUPPORTED_FILES: process.env.COPY_UNSUPPORTED_FILES !== 'false',
  LOG_LEVEL: process.env.LOG_LEVEL || 'normal'
};

// API key 管理
let currentKeyIndex = 0;
const keyUsageMap = new Map(); // 记录每个 key 的使用情况

// 设置初始 API key
if (config.API_KEYS.length > 0) {
  tinify.key = config.API_KEYS[currentKeyIndex];
}

// 命令行参数
const args = process.argv.slice(2);
const retryOnly = args.includes('--retry-failed');

// 统计信息
let stats = {
  totalFiles: 0,
  successCount: 0,
  failedCount: 0,
  skippedCount: 0,
  totalOriginalSize: 0,
  totalCompressedSize: 0,
  startTime: Date.now(),
  initialCompressionCount: 0  // 记录开始时的压缩计数
};

/**
 * 切换到下一个可用的 API key
 * @returns {boolean} 是否成功切换到新的 key
 */
async function switchToNextKey() {
  if (currentKeyIndex >= config.API_KEYS.length - 1) {
    // 没有更多的 key 可用
    return false;
  }
  
  currentKeyIndex++;
  tinify.key = config.API_KEYS[currentKeyIndex];
  
  try {
    // 验证新的 key
    await tinify.validate();
    const remaining = 500 - (tinify.compressionCount || 0);
    console.log(chalk.cyan(`\n切换到备用 API key ${currentKeyIndex + 1}/${config.API_KEYS.length}，剩余配额: ${remaining}`));
    return true;
  } catch (err) {
    // 如果这个 key 也无效，递归尝试下一个
    return await switchToNextKey();
  }
}

/**
 * 递归获取目录下所有支持格式的图片文件
 * @param {string} dir - 要扫描的目录路径
 * @returns {string[]} 所有图片文件的完整路径数组
 */
function getAllImageFiles(dir) {
  const results = [];
  
  // 跳过隐藏目录
  if (config.SKIP_HIDDEN_DIRS && path.basename(dir).startsWith('.')) {
    return results;
  }
  
  // 避免扫描输出目录和失败目录
  const absoluteDir = path.resolve(dir);
  if (absoluteDir === path.resolve(config.OUTPUT_ROOT) || 
      absoluteDir === path.resolve(config.FAILED_DIR)) {
    return results;
  }

  try {
    const files = fs.readdirSync(dir);
    
    for (const file of files) {
      const fullPath = path.join(dir, file);
      const stat = fs.statSync(fullPath);
      
      if (stat.isDirectory()) {
        results.push(...getAllImageFiles(fullPath));
      } else {
        const ext = path.extname(file).toLowerCase();
        if (config.SUPPORTED_FORMATS.includes(ext)) {
          results.push(fullPath);
        }
      }
    }
  } catch (err) {
    console.error(chalk.red(`错误：无法读取目录 ${dir}: ${err.message}`));
  }
  
  return results;
}

/**
 * 确保目录存在，如果不存在则创建
 * @param {string} dirPath - 目录路径
 */
function ensureDir(dirPath) {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true });
  }
}


/**
 * 压缩单个图片文件
 * @param {string} inputPath - 输入文件路径
 * @param {string} outputPath - 输出文件路径（替换模式下与输入路径相同）
 * @returns {Promise<Object>} 压缩结果
 */
async function compressImage(inputPath, outputPath) {
  const fileName = path.basename(inputPath);
  const originalSize = fs.statSync(inputPath).size;
  
  try {
    // 非替换模式，确保输出目录存在
    if (!config.REPLACE_ORIGINAL) {
      ensureDir(path.dirname(outputPath));
    }
    
    // 使用 TinyPNG API 压缩
    const source = tinify.fromFile(inputPath);
    
    if (config.REPLACE_ORIGINAL) {
      // 替换模式：先压缩到临时文件，成功后再替换
      const tempPath = inputPath + '.tmp';
      await source.toFile(tempPath);
      
      // 获取压缩后的文件大小
      const compressedSize = fs.statSync(tempPath).size;
      
      // 只有压缩后文件更小才替换
      if (compressedSize < originalSize) {
        fs.renameSync(tempPath, inputPath);
      } else {
        // 压缩后反而更大，删除临时文件，保持原文件
        fs.unlinkSync(tempPath);
        return {
          success: false,
          fileName,
          originalSize,
          error: '压缩后文件更大，保持原文件'
        };
      }
    } else {
      // 输出到新目录模式
      await source.toFile(outputPath);
    }
    
    // 获取压缩后的文件大小
    const compressedSize = fs.statSync(config.REPLACE_ORIGINAL ? inputPath : outputPath).size;
    const savedBytes = originalSize - compressedSize;
    const savedPercent = ((savedBytes / originalSize) * 100).toFixed(1);
    
    return {
      success: true,
      fileName,
      originalSize,
      compressedSize,
      savedBytes,
      savedPercent
    };
  } catch (err) {
    // 如果压缩失败且是替换模式，确保原文件没有被破坏
    if (config.REPLACE_ORIGINAL && fs.existsSync(inputPath + '.tmp')) {
      try {
        fs.unlinkSync(inputPath + '.tmp');
      } catch (e) {}
    }
    
    let errorMessage = '未知错误';
    
    if (err instanceof tinify.AccountError) {
      // 检查是否是配额用尽（429 错误）
      if (err.status === 429 && config.API_KEYS.length > 1) {
        // 尝试切换到下一个 key
        const switched = await switchToNextKey();
        if (switched) {
          // 递归重试当前文件
          return await compressImage(inputPath, outputPath);
        }
      }
      errorMessage = 'API key 无效或已达到使用限制';
    } else if (err instanceof tinify.ClientError) {
      errorMessage = '客户端错误：' + err.message;
    } else if (err instanceof tinify.ServerError) {
      errorMessage = '服务器错误，请稍后重试';
    } else if (err instanceof tinify.ConnectionError) {
      errorMessage = '网络连接错误';
    } else {
      errorMessage = err.message;
    }
    
    return {
      success: false,
      fileName,
      originalSize,
      error: errorMessage
    };
  }
}

/**
 * 批量压缩图片（顺序处理）
 * @param {string[]} files - 文件路径数组
 * @param {boolean} isRetry - 是否为重试模式
 */
async function batchCompress(files, isRetry = false) {
  const results = [];
  const failedFiles = [];
  
  // 创建进度指示器
  const spinner = ora({
    text: '准备开始压缩...',
    spinner: 'dots'
  }).start();
  
  // 顺序处理每个文件
  for (let i = 0; i < files.length; i++) {
    const filePath = files[i];
    const relativePath = path.relative(
      isRetry ? config.FAILED_DIR : config.SOURCE_ROOT, 
      filePath
    );
    
    let outputPath;
    if (config.REPLACE_ORIGINAL) {
      // 替换模式：输出路径就是输入路径
      outputPath = filePath;
    } else if (config.PRESERVE_DIR_STRUCTURE && !isRetry) {
      outputPath = path.join(config.OUTPUT_ROOT, relativePath);
    } else {
      outputPath = path.join(config.OUTPUT_ROOT, path.basename(filePath));
    }
    
    // 更新进度
    spinner.text = `压缩中 (${i + 1}/${files.length}): ${path.basename(filePath)}`;
    
    try {
      const result = await compressImage(filePath, outputPath);
      
      if (result.success) {
        stats.successCount++;
        stats.totalOriginalSize += result.originalSize;
        stats.totalCompressedSize += result.compressedSize;
        
        // 如果是重试模式且成功，删除失败目录中的文件
        if (isRetry) {
          try {
            fs.unlinkSync(filePath);
          } catch (err) {
            console.warn(chalk.yellow(`警告：无法删除失败文件 ${filePath}`));
          }
        }
        
        if (config.LOG_LEVEL === 'verbose') {
          spinner.succeed(
            chalk.green(`✓ ${result.fileName}: `) +
            chalk.gray(`${(result.originalSize / 1024).toFixed(1)}KB → `) +
            chalk.cyan(`${(result.compressedSize / 1024).toFixed(1)}KB `) +
            chalk.green(`(-${result.savedPercent}%)`)
          );
          spinner.start();
        }
      } else {
        stats.failedCount++;
        failedFiles.push({ path: filePath, error: result.error });
        
        if (config.LOG_LEVEL !== 'minimal') {
          spinner.fail(chalk.red(`✗ ${result.fileName}: ${result.error}`));
          spinner.start();
        }
      }
      
      results.push(result);
      
      // 文件间延迟，避免频繁调用 API
      if (i < files.length - 1 && config.FILE_DELAY > 0) {
        await new Promise(resolve => setTimeout(resolve, config.FILE_DELAY));
      }
    } catch (err) {
      // 处理意外错误
      stats.failedCount++;
      const fileName = path.basename(filePath);
      failedFiles.push({ path: filePath, error: err.message });
      
      if (config.LOG_LEVEL !== 'minimal') {
        spinner.fail(chalk.red(`✗ ${fileName}: ${err.message}`));
        spinner.start();
      }
      
      results.push({
        success: false,
        fileName,
        originalSize: 0,
        error: err.message
      });
    }
  }
  
  spinner.stop();
  
  // 处理失败文件
  if (failedFiles.length > 0 && !isRetry) {
    console.log(chalk.yellow(`\n正在保存失败文件到 ${config.FAILED_DIR}...`));
    ensureDir(config.FAILED_DIR);
    
    for (const failed of failedFiles) {
      try {
        const destPath = path.join(config.FAILED_DIR, path.basename(failed.path));
        fs.copyFileSync(failed.path, destPath);
      } catch (err) {
        console.error(chalk.red(`无法复制失败文件 ${failed.path}: ${err.message}`));
      }
    }
  }
  
  return results;
}

/**
 * 复制非压缩格式文件到输出目录
 * @param {string} dir - 源目录
 */
function copyUnsupportedFiles(dir) {
  if (!config.COPY_UNSUPPORTED_FILES) return;
  
  const copyFile = (src, dest) => {
    try {
      ensureDir(path.dirname(dest));
      fs.copyFileSync(src, dest);
      stats.skippedCount++;
      
      if (config.LOG_LEVEL === 'verbose') {
        console.log(chalk.gray(`复制: ${path.basename(src)}`));
      }
    } catch (err) {
      console.error(chalk.red(`复制文件失败 ${src}: ${err.message}`));
    }
  };
  
  const scanDir = (currentDir) => {
    // 跳过特殊目录
    if (config.SKIP_HIDDEN_DIRS && path.basename(currentDir).startsWith('.')) return;
    if (path.resolve(currentDir) === path.resolve(config.OUTPUT_ROOT)) return;
    if (path.resolve(currentDir) === path.resolve(config.FAILED_DIR)) return;
    
    try {
      const files = fs.readdirSync(currentDir);
      
      for (const file of files) {
        const fullPath = path.join(currentDir, file);
        const stat = fs.statSync(fullPath);
        
        if (stat.isDirectory()) {
          scanDir(fullPath);
        } else {
          const ext = path.extname(file).toLowerCase();
          if (!config.SUPPORTED_FORMATS.includes(ext)) {
            const relativePath = path.relative(config.SOURCE_ROOT, fullPath);
            const outputPath = path.join(config.OUTPUT_ROOT, relativePath);
            copyFile(fullPath, outputPath);
          }
        }
      }
    } catch (err) {
      console.error(chalk.red(`扫描目录失败 ${currentDir}: ${err.message}`));
    }
  };
  
  scanDir(dir);
}

/**
 * 显示压缩统计信息
 */
function showStats() {
  const duration = ((Date.now() - stats.startTime) / 1000).toFixed(1);
  const totalSaved = stats.totalOriginalSize - stats.totalCompressedSize;
  const savedPercent = stats.totalOriginalSize > 0 
    ? ((totalSaved / stats.totalOriginalSize) * 100).toFixed(1) 
    : 0;
  
  console.log(chalk.cyan('\n========== 压缩统计 =========='));
  console.log(chalk.green(`✓ 成功压缩: ${stats.successCount} 个文件`));
  
  if (stats.failedCount > 0) {
    console.log(chalk.red(`✗ 压缩失败: ${stats.failedCount} 个文件`));
  }
  
  if (stats.skippedCount > 0) {
    console.log(chalk.gray(`- 复制文件: ${stats.skippedCount} 个`));
  }
  
  console.log(chalk.yellow(`原始大小: ${(stats.totalOriginalSize / 1024 / 1024).toFixed(2)} MB`));
  console.log(chalk.green(`压缩后: ${(stats.totalCompressedSize / 1024 / 1024).toFixed(2)} MB`));
  console.log(chalk.green(`节省空间: ${(totalSaved / 1024 / 1024).toFixed(2)} MB (${savedPercent}%)`));
  console.log(chalk.blue(`总耗时: ${duration} 秒`));
  
  // 显示 API 使用情况
  if (stats.successCount > 0) {
    console.log(chalk.magenta(`\nAPI 使用情况:`));
    
    // 如果使用了多个 key，显示每个 key 的使用情况
    if (config.API_KEYS.length > 1) {
      for (let i = 0; i <= currentKeyIndex; i++) {
        if (i === currentKeyIndex) {
          // 当前 key 的使用情况
          const sessionUsed = tinify.compressionCount - stats.initialCompressionCount;
          console.log(chalk.magenta(`  Key ${i + 1}: 本次使用 ${sessionUsed} 次，总计 ${tinify.compressionCount}/500`));
        } else {
          // 之前用满的 key
          console.log(chalk.magenta(`  Key ${i + 1}: 已用满 (500/500)`));
        }
      }
    } else {
      // 单个 key 的情况
      const sessionUsed = tinify.compressionCount - stats.initialCompressionCount;
      console.log(chalk.magenta(`  本次使用: ${sessionUsed} 次`));
      console.log(chalk.magenta(`  本月总计: ${tinify.compressionCount}/500`));
    }
  }
  
  console.log(chalk.cyan('==============================\n'));
}

/**
 * 主函数
 */
async function main() {
  console.log(chalk.bold.cyan('\n🖼️  TinyPNG 图片批量压缩工具 v2.0\n'));
  
  // 验证 API key
  if (config.API_KEYS.length === 0) {
    console.error(chalk.red('错误：未设置 TinyPNG API key！'));
    console.log(chalk.yellow('请设置环境变量 TINYPNG_API_KEY 或在 .env 文件中配置'));
    console.log(chalk.gray('获取 API key: https://tinypng.com/developers'));
    process.exit(1);
  }
  
  console.log(chalk.cyan(`已配置 ${config.API_KEYS.length} 个 API key`));
  
  try {
    // 验证 API key 是否有效
    await tinify.validate();
    // validate 后 compressionCount 会被设置，记录初始值
    stats.initialCompressionCount = tinify.compressionCount || 0;
    const remaining = 500 - stats.initialCompressionCount;
    console.log(chalk.green(`✓ API key 验证成功，本月已使用: ${stats.initialCompressionCount}/500，剩余: ${remaining}`));
  } catch (err) {
    console.error(chalk.red('错误：API key 无效或网络连接失败'));
    process.exit(1);
  }
  
  if (retryOnly) {
    // 仅重试失败文件
    console.log(chalk.yellow('执行失败文件重试...\n'));
    
    if (!fs.existsSync(config.FAILED_DIR)) {
      console.log(chalk.green('没有失败文件需要重试！'));
      return;
    }
    
    const failedFiles = getAllImageFiles(config.FAILED_DIR);
    if (failedFiles.length === 0) {
      console.log(chalk.green('失败目录中没有图片文件！'));
      return;
    }
    
    stats.totalFiles = failedFiles.length;
    console.log(chalk.cyan(`找到 ${failedFiles.length} 个失败文件\n`));
    
    // 顺序重试失败文件
    await batchCompress(failedFiles, true);
    
  } else {
    // 正常压缩流程
    if (!fs.existsSync(config.SOURCE_ROOT)) {
      console.error(chalk.red(`错误：源目录不存在: ${config.SOURCE_ROOT}`));
      process.exit(1);
    }
    
    console.log(chalk.cyan(`扫描目录: ${config.SOURCE_ROOT}\n`));
    
    const imageFiles = getAllImageFiles(config.SOURCE_ROOT);
    if (imageFiles.length === 0) {
      console.log(chalk.yellow('没有找到支持的图片文件！'));
      console.log(chalk.gray(`支持的格式: ${config.SUPPORTED_FORMATS.join(', ')}`));
      return;
    }
    
    stats.totalFiles = imageFiles.length;
    console.log(chalk.cyan(`找到 ${imageFiles.length} 个图片文件\n`));
    
    // 替换模式的警告
    if (config.REPLACE_ORIGINAL) {
      console.log(chalk.yellow('⚠️  注意：替换模式已启用，将直接替换原文件！\n'));
    } else {
      // 非替换模式，创建输出目录
      ensureDir(config.OUTPUT_ROOT);
    }
    
    // 顺序压缩所有文件
    await batchCompress(imageFiles);
    
    // 复制非压缩格式文件（仅在非替换模式下）
    if (config.COPY_UNSUPPORTED_FILES && !config.REPLACE_ORIGINAL) {
      console.log(chalk.cyan('\n复制非压缩格式文件...'));
      copyUnsupportedFiles(config.SOURCE_ROOT);
    }
  }
  
  // 显示统计信息
  showStats();
  
  if (stats.failedCount > 0 && !retryOnly) {
    console.log(chalk.yellow(`失败文件已保存到: ${config.FAILED_DIR}`));
    console.log(chalk.gray('运行 npm run retry 重试失败文件\n'));
  }
  
  console.log(chalk.green('✨ 压缩完成！'));
  
  if (config.REPLACE_ORIGINAL) {
    console.log(chalk.yellow('原文件已被压缩版本替换'));
  } else {
    console.log(chalk.cyan(`压缩文件保存在: ${config.OUTPUT_ROOT}`));
  }
  
  console.log('');
}

// 错误处理
process.on('unhandledRejection', (err) => {
  console.error(chalk.red('\n发生未处理的错误:'), err);
  process.exit(1);
});

// 执行主函数
main().catch(err => {
  console.error(chalk.red('\n程序执行失败:'), err);
  process.exit(1);
});